<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset='utf-8'>
  <meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
  <meta name='mobile-web-app-capable' content='yes'>
  <meta name='apple-mobile-web-app-capable' content='yes'>

  <meta http-equiv="origin-trial"
    content="Ahfj+MLeL6bh+LNmpnSdepftxoDHHwjUG2KWZ4jjCb1WoZxtBlzF3cDHuJNVqnhr3HXJwQ+kLaw57NO15S0mRwwAAABkeyJvcmlnaW4iOiJodHRwczovL2ltbWVyc2l2ZS13ZWIuZ2l0aHViLmlvOjQ0MyIsImZlYXR1cmUiOiJXZWJYUlBsYW5lRGV0ZWN0aW9uIiwiZXhwaXJ5IjoxNjI5ODQ5NTk5fQ==">

  <title>AR Mesh Detection</title>

  <link href='../css/common.css' rel='stylesheet'>
  </link>

</head>

<body>
  <header>
    <details open>
      <summary>AR Mesh Detection</summary>
      This sample demonstrates using the
      <a href="https://immersive-web.github.io/real-world-meshing/">>Mesh Detection feature</a>
      including an implementation of synchronous hit test in JavaScript
      that leverages obtained mesh data to position objects.
      <p>
        <input id="showMeshTriangles" type="checkbox" checked>
        <label for="showMeshTriangles">Mesh triangles visible</label><br />

        <input id="useDomOverlay" type="checkbox" checked>
        <label for="useDomOverlay">Enable DOM Overlay</label><br />

        <a class="back" href="./index.html">Back</a>
      </p>
    </details>
  </header>

  <script type="importmap">
     {
        "imports": {
          "three": "https://cdn.jsdelivr.net/npm/three@0.152.2/build/three.module.js",
          "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.152.2/examples/jsm/",
          "three/examples/": "https://cdn.jsdelivr.net/npm/three@0.152.2/examples/"
        }
      }
		</script>

  <script type="module">
    // Code adapted from THREE.js' WebXR hit test sample.
    // THREE.js is covered by MIT license which can be found at:
    // https://github.com/mrdoob/THREE.js/blob/master/LICENSE

    // The code also links to a .png file from ARCore Android SDK.
    // It is covered by Apache 2.0 license which can be found at:
    // https://github.com/google-ar/arcore-android-sdk/blob/c684bbda37e44099c273c3e5274fae6fccee293c/LICENSE

    import * as THREE from 'three';
    import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.min.js';
    import { FontLoader } from 'three/addons/loaders/FontLoader.js';
    import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
    import { WebXRButton } from '../js/util/webxr-button.js';
    import { hitTest, filterHitTestResults } from '../js/hit-test.js';

    const showMeshTriangles = document.getElementById('showMeshTriangles');

    const allMeshOrigins = [];

    function updateState() {
      const createMeshMaterial = (params) =>
        new THREE.MeshBasicMaterial(Object.assign(params, {
          opacity: 0.15,
          transparent: true,
        }));

      meshMaterials.splice(0, meshMaterials.length)
      if (showMeshTriangles.checked) {
        // preallocate colors for various meshes  
        meshMaterials.push(createMeshMaterial({ color: 0xff0000 }));
        meshMaterials.push(createMeshMaterial({ color: 0x00ff00 }));
        meshMaterials.push(createMeshMaterial({ color: 0x0000ff }));
        meshMaterials.push(createMeshMaterial({ color: 0xffff00 }));
        meshMaterials.push(createMeshMaterial({ color: 0x00ffff }));
        meshMaterials.push(createMeshMaterial({ color: 0xff00ff }));
      } else {
        // if the mesh is not visible, set the blending so the mesh "punches"
        // out vr content behind it. This will occlude the VR scene with the 
        // real world.
        let material = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.FrontSide });
        material.blending = THREE.CustomBlending;
        material.blendEquation = THREE.AddEquation;
        material.blendSrc = THREE.ZeroFactor;
        material.blendDst = THREE.ZeroFactor;
        meshMaterials.push(material);
      }
    }

    showMeshTriangles.addEventListener('input', element => updateState());

    // Suppress XR events for interactions with the DOM overlay
    document.querySelector('header').addEventListener('beforexrselect', (ev) => {
      console.log(ev.type);
      ev.preventDefault();
    });

    let xrButton = null;
    let controller1, controller2;
    let controllerGrip1, controllerGrip2;

    let container;
    let camera, scene, renderer, font;
    const tempMatrix = new THREE.Matrix4();

    let reticle;
    // hitResult will be set when reticle is visible:
    let hitResult;

    const meshMaterials = [];
    const wireframeMaterial = new THREE.MeshBasicMaterial({ wireframe: true });
    const baseOriginGroup = new THREE.Group();

    const loader = new FontLoader();
    loader.load('https://cdn.jsdelivr.net/npm/three@0.152.2/examples/fonts/gentilis_regular.typeface.json', function (loaded_font){
      font = loaded_font;
      init();
    } );

		function init() {
      container = document.createElement('div');
      document.body.appendChild(container);

      // set up three.js boilerplate
      scene = new THREE.Scene();

      camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20);

      const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1);
      light.position.set(0.5, 1, 0.25);
      scene.add(light);

      renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.xr.enabled = true;
      renderer.xr.setFoveation(0);
      renderer.autoClear = false;
      container.appendChild(renderer.domElement);

      xrButton = new WebXRButton({
        onRequestSession: onRequestSession,
        onEndSession: onEndSession,
        textEnterXRTitle: "START AR",
        textXRNotFoundTitle: "AR NOT FOUND",
        textExitXRTitle: "EXIT AR",
      });

      document.querySelector('header').appendChild(xrButton.domElement);

      if (navigator.xr) {
        navigator.xr.isSessionSupported('immersive-ar')
          .then((supported) => {
            xrButton.enabled = supported;
          });
      }

      // add controllers to the scene
      controller1 = renderer.xr.getController(0);
      controller1.addEventListener('selectstart', onSelectStart);
      controller1.addEventListener('selectend', onSelectEnd);
      controller1.addEventListener('connected', function (event) {
        this.add(buildController(event.data));
      });

      controller1.addEventListener('disconnected', function () {
        this.remove(this.children[0]);
      });

      scene.add(controller1);

      controller2 = renderer.xr.getController(1);
      controller2.addEventListener('selectstart', onSelectStart);
      controller2.addEventListener('selectend', onSelectEnd);
      controller2.addEventListener('connected', function (event) {
        this.add(buildController(event.data));
      });

      scene.add(controller2);

      const controllerModelFactory = new XRControllerModelFactory();

      controllerGrip1 = renderer.xr.getControllerGrip(0);
      controllerGrip1.add(controllerModelFactory.createControllerModel(controllerGrip1));
      scene.add(controllerGrip1);

      controllerGrip2 = renderer.xr.getControllerGrip(1);
      controllerGrip2.add(controllerModelFactory.createControllerModel(controllerGrip2));
      scene.add(controllerGrip2);

      controller2.addEventListener('disconnected', function () {
        this.remove(this.children[0]);
      });
      scene.add(controller2);

      reticle = new THREE.Mesh(
        new THREE.SphereGeometry(0.15, 32, 16),
        new THREE.MeshBasicMaterial({ color: 0xff0000 })
      );

      reticle.matrixAutoUpdate = true;
      reticle.visible = false;
      scene.add(reticle);

      updateState();

      window.addEventListener('resize', onWindowResize);
    }

    function buildController(data) {

      let geometry, material;

      switch (data.targetRayMode) {
        case 'tracked-pointer':
          geometry = new THREE.BufferGeometry();
          geometry.setAttribute('position', new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, - 1], 3));
          geometry.setAttribute('color', new THREE.Float32BufferAttribute([0.5, 0.5, 0.5, 0, 0, 0], 3));

          material = new THREE.LineBasicMaterial({ vertexColors: true, blending: THREE.AdditiveBlending });

          return new THREE.Line(geometry, material);
        case 'gaze':
          geometry = new THREE.RingGeometry(0.02, 0.04, 32).translate(0, 0, - 1);
          material = new THREE.MeshBasicMaterial({ opacity: 0.5, transparent: true });
          return new THREE.Mesh(geometry, material);
      }
    }

    let pointer = undefined;

    function onSelectStart(event) {
      pointer = event.target;
    }
    function onSelectEnd(event) {
      pointer = undefined;
    }

    function draw() {
      if (pointer === undefined) {
        return;
      }

      const raycaster = new THREE.Raycaster();
      tempMatrix.identity().extractRotation(pointer.matrixWorld);

      raycaster.ray.origin.setFromMatrixPosition(pointer.matrixWorld);
      raycaster.ray.direction.set(0, 0, - 1).applyMatrix4(tempMatrix);

      allMeshes.forEach((meshContext, mesh) => {
        const intersections = raycaster.intersectObject(meshContext.mesh);
        intersections.forEach((intersection) => {
          if (intersection.object == meshContext.mesh) {
            const up = new THREE.Vector3(0, 1, 0); // reference vector
            let right = new THREE.Vector3();
            let forward = new THREE.Vector3();
            let quaternion = new THREE.Quaternion();
            let matrix = new THREE.Matrix4();

            right.crossVectors(up, intersection.face.normal);
            forward.crossVectors(right, up);

            matrix.makeBasis(right, up, forward);
            quaternion.setFromRotationMatrix(matrix);
            reticle.setRotationFromQuaternion(quaternion.normalize());
            reticle.visible = true;
            reticle.position.copy(intersection.point);
          }
        });
      });
    }

    function onRequestSession() {
      let sessionInit = {
        requiredFeatures: ['hit-test', 'mesh-detection'],
        optionalFeatures: [],
      };
      if (useDomOverlay.checked) {
        sessionInit.optionalFeatures.push('dom-overlay');
        sessionInit.domOverlay = { root: document.body };
      }
      navigator.xr.requestSession('immersive-ar', sessionInit).then((session) => {
        session.mode = 'immersive-ar';
        xrButton.setSession(session);
        onSessionStarted(session);
      });
    }

    function onSessionStarted(session) {
      useDomOverlay.disabled = true;
      session.addEventListener('end', onSessionEnded);

      renderer.xr.setReferenceSpaceType('local');
      renderer.xr.setSession(session);

      renderer.setAnimationLoop(render);
    }

    function onEndSession(session) {
      session.end();
    }

    function onSessionEnded(event) {
      useDomOverlay.disabled = false;
      xrButton.setSession(null);

      renderer.setAnimationLoop(null);
    }

    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();

      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    const geometry = new THREE.CylinderGeometry(0.1, 0.1, 0.2, 32).translate(0, 0.1, 0);

    function createGeometry(vertices, indices) {
      const geometry = new THREE.BufferGeometry();
      geometry.setIndex(new THREE.BufferAttribute(indices, 1));
      geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));

      return geometry;
    }

    let meshId = 1;
    let allMeshes = new Map();
    // Iterate over the meshes array and compare to our internal state
    // This is the code that keeps track of the mesh state and adds them to the scene.
    function processMeshes(timestamp, frame) {
      const referenceSpace = renderer.xr.getReferenceSpace();

      if (frame.detectedMeshes) {
        allMeshes.forEach((meshContext, mesh) => {
          // if a previous mesh is no longer reported
          if (!frame.detectedMeshes.has(mesh)) {
            // mesh was removed
            allMeshes.delete(mesh);
            console.debug("Mesh no longer tracked, id=" + meshContext.id);

            scene.remove(meshContext.mesh);
            scene.remove(meshContext.wireframe);
            scene.remove(meshContext.text);
          }
        });

        // compare all incoming meshes with our internal state
        frame.detectedMeshes.forEach(mesh => {
          const meshPose = frame.getPose(mesh.meshSpace, referenceSpace);
          let meshMesh;
          let wireframeMesh;
          let textMesh;

          // this is a mesh we've seen before
          if (allMeshes.has(mesh)) {
            // may have been updated:
            const meshContext = allMeshes.get(mesh);
            meshMesh = meshContext.mesh;
            wireframeMesh = meshContext.wireframe;

            if (meshContext.timestamp < mesh.lastChangedTime) {
              // the mesh was updated!
              meshContext.timestamp = mesh.lastChangedTime;

              const geometry = createGeometry(mesh.vertices, mesh.indices);
              meshContext.mesh.geometry.dispose();
              meshContext.mesh.geometry = geometry;
              meshContext.wireframe.geometry.dispose();
              meshContext.wireframe.geometry = geometry;
            }
          } else {
            // new mesh

            // Create geometry:
            const geometry = createGeometry(mesh.vertices, mesh.indices);

            wireframeMesh = new THREE.Mesh(geometry, wireframeMaterial);
            wireframeMesh.matrixAutoUpdate = false;
            scene.add(wireframeMesh);

            meshMesh = new THREE.Mesh(geometry,
              meshMaterials[meshId % meshMaterials.length]
            );
            meshMesh.matrixAutoUpdate = false;
            scene.add(meshMesh);
            if (mesh.semanticLabel && mesh.semanticLabel.length > 0) {
              const shapes = font.generateShapes(mesh.semanticLabel, 0.1);
              const geometry = new THREE.ShapeGeometry(shapes);
              geometry.computeBoundingBox();
              const xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
              geometry.translate(xMid, 0, 0);
              textMesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());

              scene.add(textMesh);
            }
            const originGroup = baseOriginGroup.clone();
            originGroup.visible = false;

            meshMesh.add(originGroup);
            allMeshOrigins.push(originGroup);

            const meshContext = {
              id: meshId,
              timestamp: mesh.lastChangedTime,
              mesh: meshMesh,
              wireframe: wireframeMesh,
              text: textMesh,
              origin: originGroup,
            };

            allMeshes.set(mesh, meshContext);
            console.debug("New mesh detected, id=" + meshId);
            meshId++;
          }

          if (meshPose) {
            meshMesh.visible = true;
            meshMesh.matrix.fromArray(meshPose.transform.matrix);
            wireframeMesh.visible = showMeshTriangles.checked;
            wireframeMesh.matrix.fromArray(meshPose.transform.matrix);
            if (textMesh) {
              textMesh.visible = true;
              const position = new THREE.Vector3().setFromMatrixPosition(wireframeMesh.matrix);
              textMesh.position.copy(position);
            }
          } else {
            meshMesh.visible = false;
            wireframeMesh.visible = false;
            if (textMesh) {
              textMesh.visible = false;
            }
          }
        });

        allMeshes.forEach((meshContext, mesh) => {
          if (meshContext.text) {
            const direction = new THREE.Vector3().subVectors(camera.position, meshContext.text.position).normalize();
            meshContext.text.lookAt(direction);
          }
        });
      }
    }

    function render(timestamp, frame) {
      if (frame) {
        processMeshes(timestamp, frame);
        draw();

        renderer.render(scene, camera);
      }
    }

  </script>
</body>

</html>